import datetime
import os
import sys
import tkinter as tk
import tkinter.messagebox as messagebox

from tkinter import ttk
from typing import List, Dict

from scripts.BackupRestore import BackupRestore
from scripts.Log import Log
from scripts.util.Util import Util
from scripts.device.AdbDevice import AdbDevice
from scripts.ui.DevicesDialog import DevicesDialog
from scripts.ui.Window import Window


class MainWindow(Window):
    VERSION = 'v1.14.2'

    STATE_IDLE = 1
    STATE_IDLE_READY = 2
    STATE_BACKUP = 3
    STATE_RESTORE = 4

    _SCRIPT_DIR: str = os.path.dirname(sys.argv[0])
    _DATA_DIR: str = os.path.join(_SCRIPT_DIR, 'data')

    STEP_LIST = (
        BackupRestore.STEP_PREPARE,
        BackupRestore.STEP_BACKUP_USER,
        BackupRestore.STEP_BACKUP_MISC,
        BackupRestore.STEP_BACKUP_APP,
        BackupRestore.STEP_BACKUP_SYSTEM,
        BackupRestore.STEP_BACKUP_APP_DATA,
        BackupRestore.STEP_CLEANUP
    )
    STEP_ENABLE_LIST = {
        BackupRestore.STEP_PREPARE: False,
        BackupRestore.STEP_BACKUP_USER: True,
        BackupRestore.STEP_BACKUP_APP: True,
        BackupRestore.STEP_BACKUP_ADB_BACKUP: True,
        BackupRestore.STEP_BACKUP_APP_DATA: True,
        BackupRestore.STEP_BACKUP_SYSTEM: True,
        BackupRestore.STEP_BACKUP_MISC: True,
        BackupRestore.STEP_CLEANUP: False
    }

    STEP_DEFAULT_CHECK_LIST = {
        BackupRestore.STEP_PREPARE: True,
        BackupRestore.STEP_BACKUP_USER: True,
        BackupRestore.STEP_BACKUP_APP: True,
        BackupRestore.STEP_BACKUP_ADB_BACKUP: True,
        BackupRestore.STEP_BACKUP_APP_DATA: True,
        BackupRestore.STEP_BACKUP_SYSTEM: True,
        BackupRestore.STEP_BACKUP_MISC: True,
        BackupRestore.STEP_CLEANUP: True
    }

    def __init__(self):
        super().__init__('Backup/Restore %s' % MainWindow.VERSION)
        Log.i('Version: %s' % MainWindow.VERSION)
        self._state = MainWindow.STATE_IDLE
        self._last_message = 'Ready'

        self.win().geometry("500x400+200+100")

        self.current_steps: List[str] = []

        self.style_msg_highlight = ttk.Style()
        self.style_msg_highlight.configure('Highlight.TLabel', foreground='red')

        self.style_msg_normal = ttk.Style()
        self.style_msg_normal.configure('Normal.TLabel', foreground='black')

        # backup/restore button
        frame_btn = tk.Frame(self.win())
        frame_btn.pack(fill=tk.X, pady=10, padx=10)

        self.btn_backup = ttk.Button(frame_btn,
                                     command=lambda: Util.async_task(self._on_backup),
                                     text='Backup')
        self.btn_backup.pack(side=tk.LEFT, padx=10)

        self.btn_restore = ttk.Button(frame_btn,
                                      command=lambda: Util.async_task(self._on_restore),
                                      text='Restore')
        self.btn_restore.pack(side=tk.LEFT, padx=10)

        # device list
        frame_device = ttk.Frame(self.win())
        frame_device.pack(fill=tk.X, pady=10, padx=10)

        label_device = ttk.Label(frame_device,
                                 text='Device:')
        label_device.pack(side=tk.LEFT)

        self.current_device = tk.StringVar()
        self.cb_device_list = ttk.Combobox(frame_device,
                                           textvariable=self.current_device)
        self.cb_device_list.pack(fill=tk.X, side=tk.LEFT, padx=10)

        self.btn_refresh = ttk.Button(frame_device,
                                      text='Refresh',
                                      command=lambda: self._update_devices())
        self.btn_refresh.pack(side=tk.LEFT, padx=5)

        self.btn_stored_backup = ttk.Button(frame_device,
                                            text='Show backup devices',
                                            command=lambda: self._show_backups())
        self.btn_stored_backup.pack(side=tk.RIGHT, padx=5)

        # infos like state & progress
        frame_info = ttk.Frame(self.win())
        frame_info.pack(fill=tk.X, anchor='w', pady=10, padx=10)

        self.msg_state = ttk.Label(frame_info,
                                   justify='center',
                                   anchor='center')
        self.msg_state.pack(anchor='w')

        # check boxes
        frame_steps = ttk.Frame(self.win())
        frame_steps.pack(fill=tk.X, anchor='w', pady=10, padx=10)

        self.steps: List[tk.StringVar] = []
        self.step_status: Dict[str, ttk.Label] = dict()
        self.actions: List[ttk.Checkbutton] = []

        for step in MainWindow.STEP_LIST:
            row = len(self.steps)
            var = tk.StringVar()
            self.steps.append(var)

            step_item: BackupRestore.Step = BackupRestore.STEPS[step]
            action = ttk.Checkbutton(frame_steps,
                                     text=step_item.display_name(),
                                     variable=var,
                                     onvalue=step_item.key(),
                                     offvalue='')
            if MainWindow.STEP_DEFAULT_CHECK_LIST[step]:
                action.invoke()
            if MainWindow.STEP_ENABLE_LIST[step]:
                self.actions.append(action)
            action.config(state=tk.NORMAL if MainWindow.STEP_ENABLE_LIST[step] else tk.DISABLED)
            action.grid(row=row, column=0, sticky='w')

            action_status = ttk.Label(frame_steps)
            action_status.grid(row=row, column=1, sticky='w')
            self.step_status[step] = action_status

        self._update_ui()
        self._update_devices()

    def _set_state(self, state: int):
        if self._state != state:
            self._state = state
            self._update_ui()

    def _update_ui(self):
        if self._state == MainWindow.STATE_IDLE:
            self.btn_backup.config(state=tk.DISABLED)
            self.btn_restore.config(state=tk.DISABLED)
            self.cb_device_list.config(state=tk.NORMAL)
            self.btn_refresh.config(state=tk.NORMAL)
            for action in self.actions:
                action.config(state=tk.NORMAL)
            self.msg_state.config(text='State: Idle (%s)' % self._last_message)
        elif self._state == MainWindow.STATE_IDLE_READY:
            self.btn_backup.config(state=tk.NORMAL)
            self.btn_restore.config(state=tk.NORMAL)
            self.cb_device_list.config(state=tk.NORMAL)
            self.btn_refresh.config(state=tk.NORMAL)
            for action in self.actions:
                action.config(state=tk.NORMAL)
            self.msg_state.config(text='State: Idle (%s)' % self._last_message)
        elif self._state == MainWindow.STATE_BACKUP:
            self.btn_backup.config(state=tk.DISABLED)
            self.btn_restore.config(state=tk.DISABLED)
            self.cb_device_list.config(state=tk.DISABLED)
            self.btn_refresh.config(state=tk.DISABLED)
            for action in self.actions:
                action.config(state=tk.DISABLED)
            self.msg_state.config(text='State: Backup...')
        elif self._state == MainWindow.STATE_RESTORE:
            self.btn_backup.config(state=tk.DISABLED)
            self.btn_restore.config(state=tk.DISABLED)
            self.cb_device_list.config(state=tk.DISABLED)
            self.btn_refresh.config(state=tk.DISABLED)
            for action in self.actions:
                action.config(state=tk.DISABLED)
            self.msg_state.config(text='State: Restore...')

    def _update_devices(self):
        devices = AdbDevice.devices()
        self.cb_device_list.config(values=devices)
        if len(devices) > 0:
            if self.current_device.get() not in devices:
                self.current_device.set(devices[0])
            if self._state == MainWindow.STATE_IDLE:
                self._set_state(MainWindow.STATE_IDLE_READY)
        else:
            self.current_device.set('')
            if self._state == MainWindow.STATE_IDLE_READY:
                self._set_state(MainWindow.STATE_IDLE)

    def _steps_from_ui(self) -> List[str]:
        return list(filter(lambda item: item != '', [check.get() for check in self.steps]))

    def _show_backups(self):
        backups = os.listdir(MainWindow._DATA_DIR)

        backups = [path
                   if Util.is_backup_done(os.path.join(MainWindow._DATA_DIR, path))
                   else '%s [Invalid]' % path
                   for path in backups]

        DevicesDialog(self.win(), backups).mainloop()

    def _on_backup(self):
        if self.current_device.get() == '':
            messagebox.showerror(title='Error', message='Please insert an adb device')
            return

        begin_time = datetime.datetime.now()

        self._set_state(MainWindow.STATE_BACKUP)
        # reset step status
        for step in self.step_status:
            self.step_status[step].config(text='')

        # check backup path
        self.current_steps = self._steps_from_ui()
        sn = self.current_device.get()
        backup_path = os.path.join(MainWindow._DATA_DIR, sn)
        if os.path.exists(backup_path):
            Log.d('Remove old backup of [%s]' % sn)
            Util.rmdirs(backup_path)
        os.makedirs(backup_path, exist_ok=True)

        # do backup
        BackupRestore(sn,
                      backup_path,
                      self.current_steps).backup(self._on_backup_progress)

        # mark done
        Util.mark_backup_done(backup_path)

        # update UI state
        duration_time = datetime.datetime.now() - begin_time
        minutes = duration_time.seconds // 60
        seconds = duration_time.seconds % 60
        ms = duration_time.microseconds // 1000
        self._last_message = 'Backup success!! [%02d:%02d.%03d]' % (minutes, seconds, ms)
        if self.current_device.get() == '':
            self._set_state(MainWindow.STATE_IDLE)
        else:
            self._set_state(MainWindow.STATE_IDLE_READY)

    def _on_restore(self):
        if self.current_device.get() == '':
            messagebox.showerror(title='Error', message='Please insert an adb device')
            return

        begin_time = datetime.datetime.now()

        sn = self.current_device.get()
        if self.current_device.get() == '':
            messagebox.showerror(title='Error', message='Please insert an adb device')
            return

        backup_path = os.path.join(MainWindow._DATA_DIR, sn)
        if not os.path.exists(backup_path):
            messagebox.showerror(title='Error', message='Please backup device [%s] first' % sn)
            return

        if not Util.is_backup_done(backup_path):
            messagebox.showerror(title='Error', message='Backup of [%s] is invalid. Please backup first' % sn)
            return

        self._set_state(MainWindow.STATE_RESTORE)
        # reset step status
        for step in self.step_status:
            self.step_status[step].config(text='')

        # do restore
        self.current_steps = self._steps_from_ui()
        BackupRestore(sn,
                      backup_path,
                      self.current_steps).restore(self._on_restore_progress)

        # update UI state
        duration_time = datetime.datetime.now() - begin_time
        minutes = duration_time.seconds // 60
        seconds = duration_time.seconds % 60
        ms = duration_time.microseconds // 1000
        self._last_message = 'Restore success!! [%02d:%02d.%03d]' % (minutes, seconds, ms)
        if self.current_device.get() == '':
            self._set_state(MainWindow.STATE_IDLE)
        else:
            self._set_state(MainWindow.STATE_IDLE_READY)

    def _on_backup_progress(self, cur_prog: int, max_prog: int, msg: str,
                            sub_cur_prog: int, sub_max_prog: int, sub_msg: str):
        if cur_prog < len(self.current_steps):
            if sub_msg.startswith('!!'):
                self.step_status[self.current_steps[cur_prog]].config(
                    style='Highlight.TLabel',
                    text='[%d / %d] %s' % (sub_cur_prog, sub_max_prog, sub_msg[2:]))
            else:
                self.step_status[self.current_steps[cur_prog]].config(
                    style='Normal.TLabel',
                    text='[%d / %d] %s' % (sub_cur_prog, sub_max_prog, sub_msg))

    def _on_restore_progress(self, cur_prog: int, max_prog: int, msg: str,
                             sub_cur_prog: int, sub_max_prog: int, sub_msg: str):
        if cur_prog < len(self.current_steps):
            if sub_msg.startswith('!!'):
                self.step_status[self.current_steps[cur_prog]].config(
                    style='Highlight.TLabel',
                    text='[%d / %d] %s' % (sub_cur_prog, sub_max_prog, sub_msg[2:]))
            else:
                self.step_status[self.current_steps[cur_prog]].config(
                    style='Normal.TLabel',
                    text='[%d / %d] %s' % (sub_cur_prog, sub_max_prog, sub_msg))
